#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

#include <tox/tox.h>
#include <tox/toxencryptsave.h>

struct tox_self {
	uint8_t address[TOX_ADDRESS_SIZE];
	uint32_t nospam;
	uint8_t public_key[TOX_PUBLIC_KEY_SIZE];
	uint8_t secret_key[TOX_SECRET_KEY_SIZE];
	uint8_t name[TOX_MAX_NAME_LENGTH];
	size_t name_size;
	uint8_t status_message[TOX_MAX_STATUS_MESSAGE_LENGTH];
	size_t status_message_size;
	enum TOX_USER_STATUS status;
};

/* returns 0 on failure */
int
get_self_data(const Tox *tox, struct tox_self *self)
{
	if (!tox || !self) {
		fprintf(stderr, "%s: !tox || !self\n", __func__);
		return 0;
	}

	tox_self_get_address(tox, self->address);
	self->nospam = tox_self_get_nospam(tox);
	tox_self_get_public_key(tox, self->public_key);
	tox_self_get_secret_key(tox, self->secret_key);
	tox_self_get_name(tox, self->name);
	self->name_size = tox_self_get_name_size(tox);
	tox_self_get_status_message(tox, self->status_message);
	self->status_message_size = tox_self_get_status_message_size(tox);

	return !0;
}

char *
tox_err_options_new_str(enum TOX_ERR_OPTIONS_NEW error)
{
	switch (error) {
	case TOX_ERR_OPTIONS_NEW_OK:
		return "Success";
	case TOX_ERR_OPTIONS_NEW_MALLOC:
		return "Out of memory";
	}

	return "Unknown error";
}

char *
tox_err_new_str(enum TOX_ERR_NEW error)
{
	switch (error) {
	case TOX_ERR_NEW_OK:
		return "Success";
	case TOX_ERR_NEW_NULL:
		return "Unexpected argument NULL";
	case TOX_ERR_NEW_MALLOC:
		return "Out of memory";
	case TOX_ERR_NEW_PORT_ALLOC:
		return "Unable to bind to port";
	case TOX_ERR_NEW_PROXY_BAD_TYPE:
		return "Invalid proxy type";
	case TOX_ERR_NEW_PROXY_BAD_HOST:
		return "Invalid proxy host";
	case TOX_ERR_NEW_PROXY_BAD_PORT:
		return "Invalid proxy port";
	case TOX_ERR_NEW_PROXY_NOT_FOUND:
		return "Proxy not found";
	case TOX_ERR_NEW_LOAD_ENCRYPTED:
		return "Data encrypted";
	case TOX_ERR_NEW_LOAD_BAD_FORMAT:
		return "Bad data format";
	};

	return "Unknown error";
}

char *
tox_err_decryption_str(enum TOX_ERR_DECRYPTION error)
{
	switch (error) {
	case TOX_ERR_DECRYPTION_OK:
		return "Success";
	case TOX_ERR_DECRYPTION_NULL:
		return "Unexpected argument NULL";
	case TOX_ERR_DECRYPTION_INVALID_LENGTH:
		return "Data too short";
	case TOX_ERR_DECRYPTION_BAD_FORMAT:
		return "Bad data format";
	case TOX_ERR_DECRYPTION_KEY_DERIVATION_FAILED:
		return "Key derivation failed";
	case TOX_ERR_DECRYPTION_FAILED:
		return "Wrong passphrase";
	}

	return "Unknown error";
}

char *
bin2hex(char *hexstr, const uint8_t *binstr, size_t binstr_size)
{
	static const char hex_alphabet[] = "0123456789ABCDEF";
	uint8_t high, low;
	size_t i;

	for (i=0; i<binstr_size; ++i) {
		high = binstr[i] >> 4 & 0xF;
		low  = binstr[i]      & 0XF;
		hexstr[i * 2]     = hex_alphabet[high];
		hexstr[i * 2 + 1] = hex_alphabet[low];
	}
	hexstr[i * 2] = '\0';

	return hexstr;
}

/* returned pointer must be free'd by the caller */
uint8_t *
get_savedata(char *tox_file_name, size_t *tox_file_size)
{
	FILE *tox_file;
	uint8_t *savedata;
	size_t savedata_size;

	if (!tox_file_name || !tox_file_size) {
		fprintf(stderr, "%s: Invalid argument.", __func__);
		return NULL;
	}

	tox_file = fopen(tox_file_name, "r");
	if (!tox_file) {
		perror(tox_file_name);
		return NULL;
	}

	fseek(tox_file, 0, SEEK_END);
	savedata_size = ftell(tox_file);
	savedata = malloc(savedata_size);
	if (!savedata) {
		perror("savedata");
		fclose(tox_file);
		return NULL;
	}
	fseek(tox_file, 0, SEEK_SET);

	errno = 0;
	fread(savedata, savedata_size, 1, tox_file);
	if (errno) {
		perror(tox_file_name);
		free(savedata);
		fclose(tox_file);
		return NULL;
	}

	fclose(tox_file);

	*tox_file_size = savedata_size;
	return savedata;
}

void
print_self(const struct tox_self *self)
{
	char addr_hex[TOX_ADDRESS_SIZE * 2 + 1];
	char pubkey_hex[TOX_PUBLIC_KEY_SIZE * 2 + 1];
	char seckey_hex[TOX_SECRET_KEY_SIZE * 2 + 1];

	if (!self) {
		fprintf(stderr, "%s: !self\n", __func__);
		return;
	}

	bin2hex(addr_hex, self->address, sizeof(self->address));
	bin2hex(pubkey_hex, self->public_key, sizeof(self->public_key));
	bin2hex(seckey_hex, self->secret_key, sizeof(self->secret_key));

	printf("name: \"%.*s\"\n", self->name_size, self->name);
	printf("status_message: \"%.*s\"\n", self->status_message_size, self->status_message);
	printf("address: \"%s\"\n", addr_hex);
	printf("public key: \"%s\"\n", pubkey_hex);
	printf("secret key: \"%s\"\n", seckey_hex);
	printf("nospam: %X\n", self->nospam);
}

void
print_friend(const Tox *tox, uint32_t friend_num)
{
	uint8_t pubkey[TOX_PUBLIC_KEY_SIZE];
	char pubkey_hex[TOX_PUBLIC_KEY_SIZE * 2 + 1];
	uint64_t last_online;
	uint8_t *name;
	size_t name_size;
	uint8_t *status_msg;
	size_t status_msg_size;

	if (!tox) {
		fprintf(stderr, "%s: !tox\n", __func__);
		return;
	}

	if (!tox_friend_exists(tox, friend_num)) {
		fprintf(stderr, "Friend #%u doesn't exist.\n", friend_num);
		return;
	}

	tox_friend_get_public_key(tox, friend_num, pubkey, NULL);
	bin2hex(pubkey_hex, pubkey, sizeof(pubkey));
	printf("public key: \"%s\"\n", pubkey_hex);

	last_online = tox_friend_get_last_online(tox, friend_num, NULL);
	/* ctime already fucking prints a newline */
	printf("Last online: %s", ctime(&last_online));

	name_size = tox_friend_get_name_size(tox, friend_num, NULL);
	name = malloc(name_size);
	if (name) {
		tox_friend_get_name(tox, friend_num, name, NULL);
		printf("name: \"%.*s\"\n", name_size, name);
		free(name);
	}

	status_msg_size = tox_friend_get_status_message_size(tox, friend_num, NULL);
	status_msg = malloc(status_msg_size);
	if (status_msg) {
		tox_friend_get_status_message(tox, friend_num, status_msg, NULL);
		printf("status message: \"%.*s\"\n", status_msg_size, status_msg);
		free(status_msg);
	}
}

void
print_friend_list(const Tox *tox)
{
	uint32_t *flist;
	size_t flist_size;
	size_t i;

	if (!tox) {
		fprintf(stderr, "%s: !tox\n", __func__);
		return;
	}

	flist_size = tox_self_get_friend_list_size(tox);
	flist = calloc(flist_size, sizeof(uint32_t));
	if (!flist) {
		perror("flist");
		exit(1);
	}
	tox_self_get_friend_list(tox, flist);

	printf("Friend list size: %zu\n", flist_size);
	for (i=0; i<flist_size; ++i) {
		printf("Friend #%u:\n", flist[i]);
		print_friend(tox, flist[i]);
		if (i+1 != flist_size) {
			printf("\n");
		}
	}

	free(flist);
}

/*
 * status:
 *    -1: previous
 *     0: disable
 *  else: enable
 */
void
stdin_set_echo(int status)
{
	static struct termios term_prev;
	static cc_t cc[NCCS];
	struct termios term;
	struct termios term_swap;

	if (tcgetattr(STDIN_FILENO, &term) < 0) {
		if (ENOTTY == errno) {
			return;
		}
		perror("tcgetattr");
		exit(1);
	}

	if (-1 == status) {
		fprintf(stderr, "Resetting term settings.\n");
		term_swap = term;
		term = term_prev;
		term_prev = term_swap;
		for (size_t i=0; i<sizeof(cc); ++i) {
			term.c_cc[i] = cc[i];
		}
		goto setattr;
	}

	term_prev = term;
	for (size_t i=0; i<sizeof(cc); ++i) {
		cc[i] = term.c_cc[i];
	}

#define TERMFLAGS (ICANON|ECHO|ECHOE|ECHOK|ECHONL)
	if (!status) {
		fprintf(stderr, "Disabling echo.\n");
		term.c_lflag &= ~ECHO;
	} else {
		fprintf(stderr, "Enabling echo.\n");
		term.c_lflag |= ECHO;
	}

setattr:
	if (tcsetattr(STDIN_FILENO, TCSANOW, &term)) {
		perror("tcsetattr");
		exit(1);
	}
}

void
stdin_enable_echo(void)
{
	struct termios term;

	if (tcgetattr(STDIN_FILENO, &term) < 0) {
		perror("tcgetattr");
		exit(1);
	}

	term.c_lflag |= ECHO;

	if (tcsetattr(STDIN_FILENO, TCSANOW, &term) < 0) {
		perror("tcsetattr");
		exit(1);
	}
}

void
stdin_disable_echo(void)
{
	struct termios term;

	if (tcgetattr(STDIN_FILENO, &term) < 0) {
		perror("tcgetattr");
		exit(1);
	}

	term.c_lflag &= ~ECHO;

	if (tcsetattr(STDIN_FILENO, TCSANOW, &term) < 0) {
		perror("tcsetattr");
		exit(1);
	}


/* 	tcflush(STDIN_FILENO, TCIOFLUSH);
 * 	if (tcsetattr(STDIN_FILENO, TCSANOW, &term) < 0) {
 * 		perror("tcsetattr");
 * 		exit(1);
 * 	}
 */
}

void stdin_print(int id)
{
	struct termios term;
	size_t i;

	if (tcgetattr(STDIN_FILENO, &term) < 0) {
		perror("tcgetattr");
		exit(1);
	}

	fprintf(stderr, "--- %d\n", id);
	fprintf(stderr, "c_iflag: 0x%x\n", term.c_iflag);
	fprintf(stderr, "c_oflag: 0x%x\n", term.c_oflag);
	fprintf(stderr, "c_cflag: 0x%x\n", term.c_cflag);
	fprintf(stderr, "c_lflag: 0x%x\n", term.c_lflag);
	fprintf(stderr, "c_line:  0x%x\n", term.c_line);
	fprintf(stderr, "c_cc: ");
	for (i=0; i<NCCS; ++i) {
		fprintf(stderr, "%x", term.c_cc[i]);
	}
	fprintf(stderr, "\n");
}

/* return length of password on success, 0 on failure */
static int password_prompt(char *buf, int size)
{
    buf[0] = '\0';

    /* disable terminal echo */
    struct termios oflags, nflags;
    tcgetattr(fileno(stdin), &oflags);
    nflags = oflags;
    nflags.c_lflag &= ~ECHO;
    nflags.c_lflag |= ECHONL;

    if (tcsetattr(fileno(stdin), TCSANOW, &nflags) != 0)
        return 0;

    const char *p = fgets(buf, size, stdin);
    int len = strlen(buf);

    /* re-enable terminal echo */
    tcsetattr(fileno(stdin), TCSANOW, &oflags);

    if (p == NULL || len <= 1)
        return 0;

    /* eat overflowed stdin and return error */
    if (buf[--len] != '\n') {
        int ch;

        while ((ch = getchar()) != '\n' && ch > 0)
            ;

        return 0;
    }

    buf[len] = '\0';
    return len;
}

int
main(int argc, char *argv[])
{
	struct Tox_Options *options;
	Tox *tox;
	struct tox_self tox_self;
	uint8_t *savedata;
	size_t savedata_size;

	if (argc != 2) {
		fprintf(stderr, "Usage: %s <tox file>\n", argv[0]);
		return 1;
	}

	savedata = get_savedata(argv[1], &savedata_size);
	if (!savedata) {
		fprintf(stderr, "Couldn't get savedata from file.\n");
		exit(1);
	}
	if (savedata_size > TOX_PASS_ENCRYPTION_EXTRA_LENGTH
	    && tox_is_data_encrypted(savedata))
	{
		uint8_t *plain;
		char pass[BUFSIZ] = { 0 };
		size_t pass_len;
		enum TOX_ERR_DECRYPTION error;

		plain = malloc(savedata_size - TOX_PASS_ENCRYPTION_EXTRA_LENGTH);
		if (!plain) {
			perror("plain");
			exit(1);
		}

#if 1
		stdin_print(0);
		fprintf(stderr, "Enter passphrase: ");
# if 1
		stdin_set_echo(0);
# else
		stdin_disable_echo();
# endif
		stdin_print(1);
		if (!fgets(pass, sizeof(pass), stdin)) {
			perror("stdin");
			fprintf(stderr, "An error occured.\n");
			tcflush(STDIN_FILENO, TCIOFLUSH);
# if 1
			stdin_set_echo(-1);
# else
			stdin_enable_echo();
# endif
			stdin_print(21);
			exit(1);
		}
		if (tcflush(STDIN_FILENO, TCIOFLUSH) < 0) {
			perror("tcflush");
		}
# if 1
		stdin_set_echo(-1);
# else
		stdin_enable_echo();
# endif
		stdin_print(22);
		if (tcflush(STDIN_FILENO, TCIOFLUSH) < 0) {
			perror("tcflush");
		}
#else
		password_prompt(pass, sizeof(pass));
#endif
		pass_len = strcspn(pass, "\n");
		pass[pass_len] = '\0';

		if (!tox_pass_decrypt(savedata, savedata_size,
		                      (uint8_t *)pass, pass_len,
		                      plain, &error))
		{
			fprintf(stderr, "tox_pass_decrypt: %s\n", tox_err_decryption_str(error));
			fclose(stdout);
#if 0
			while (1) {
				usleep(1000);
			}
#endif
			stdin_set_echo(-1);
			stdin_set_echo(-1);
			stdin_print(3);
			exit(1);
		}

		free(savedata);
		savedata = plain;
		savedata_size -= TOX_PASS_ENCRYPTION_EXTRA_LENGTH;
	}
	{
		enum TOX_ERR_OPTIONS_NEW error;

		options = tox_options_new(&error);
		if (!options || error) {
			fprintf(stderr, "tox_options_new: %s\n", tox_err_options_new_str(error));
			exit(1);
		}
	}
	tox_options_set_savedata_type(options, TOX_SAVEDATA_TYPE_TOX_SAVE);
	tox_options_set_savedata_data(options, savedata, savedata_size);
	{
		enum TOX_ERR_NEW error;

		tox = tox_new(options, &error);
		if (!tox || error) {
			fprintf(stderr, "tox_new: %s\n", tox_err_new_str(error));
			exit(1);
		}
	}
	tox_options_free(options);
	free(savedata);
	/*  */
	get_self_data(tox, &tox_self);
	printf("-=*[ Self ]*=-\n");
	print_self(&tox_self);
	printf("\n");
	printf("-=*[ Friend list ]*=-\n");
	print_friend_list(tox);
	fclose(stdout);
	/*  */
	tox_kill(tox);

	return 0;
}
